package sun.scheduler.core.resource;

import java.util.TreeMap;

/**
 * Represents a Ressource profile which matches the used cost for a given time
 * intervall and provides methods to check if they are contained within the
 * boundries of a specified capacity.
 *
 * @author mfj
 */
public class ResourceProfile
{

    private long maxCapacity;
    private long minAvailableResources;
    private Resource resource;
    protected TreeMap<Long, Integer> profile;

    /**
     * Instantiates a new {@link ResourceProfile} for the specified
     * {@link Resource}.
     *
     * @param resource the resource of this profile.
     * @param maxCpacity the max capacity of this profile.
     */
    public ResourceProfile(Resource resource, int maxCpacity)
    {
        this.maxCapacity = maxCpacity;
        this.minAvailableResources = maxCapacity;
        this.resource = resource;
        this.profile = new TreeMap<Long, Integer>();
    }

    /**
     * Instantiates a new {@link ResourceProfile} for the specified
     * {@link Resource}.
     *
     * @param resource the resource of this profile.
     */
    public ResourceProfile(Resource resource)
    {
        this(resource, resource.maxCapacity);
    }

    /**
     * Gets the max. cost allowed to spend.
     *
     * @return
     */
    public long getMaxCapacity()
    {
        return maxCapacity;
    }

    /**
     * Gets the minimal amount of available resources over all set time
     * intervalls.
     *
     * @return the min available resources
     */
    public long getMinAvailableResources()
    {
        return minAvailableResources;
    }

    /**
     * Sets the maximal amount of cost allowed to spend.
     *
     * @param capacity
     * @throws IllegalArgumentException if {@code capacity <= 0}
     */
    public void setMaxCapacity(long capacity)
    {
        if (capacity <= 0)
            throw new IllegalArgumentException("Nonpositive capacity.");

        this.maxCapacity = capacity;
        this.minAvailableResources += capacity - maxCapacity;
    }

    /**
     * Gets the resource assoziated with this instance.
     *
     * @return
     */
    public Resource getResource()
    {
        return resource;
    }

    /**
     * Increases the total cost used between the intervall of the given times
     * start to start + duration by the specified value. And returns the max
     * cost spend.
     *
     * I.e. if the cost of [0,3] is 2 and from ]3, 5] is 1 the call {@code pack(1,3,2)}
     * would set the cost as follows: [0,1[ -> 2, [1,3]->4, ]3,4]->3, }4,5]->1
     * the returned value would be 4.
     *
     * Note that this Method does not check if the total cost is below the
     * capacity.
     *
     * @param start the start time of the intervall
     * @param duration length of the intervall
     * @param value the value which is to increase
     * @return the maximum amount spend.
     * @throws IllegalStateException if this profile is blocked.
     */
    public int pack(long start, long duration, int value)
    {
        int maxCost = Integer.MIN_VALUE;
        // we need mark the time before the start with its old costs
        // this will enable us to differentiate between  values of the
        // overwritten intervalls.

        long end = start + duration;

        int newStartCost = cost(start) + value;
        int newEndCost = cost(end) + value;

        profile.put(start, newStartCost);
        profile.put(end, newEndCost);

        maxCost = Math.max(maxCost, newStartCost);
        maxCost = Math.max(maxCost, newEndCost);

        // increase values of all jumps between ]start, end[, 
        // both points not included
        Long currentJump = start;
        while ((currentJump = profile.higherKey(currentJump)) != null && currentJump < end)
        {
            int newValue = profile.get(currentJump) + value;
            profile.put(currentJump, newValue);
            if (newValue > maxCost)
                maxCost = newValue;
        }

        minAvailableResources = Math.min(minAvailableResources, maxCapacity - maxCost);
        return maxCost;
    }

    /**
     * Gets the total cost of the given time if the time was not set then
     * getCost should return zero {@code 0}.
     *
     * @param time the time of which to get the cost.
     */
    public int cost(long time)
    {
        if (profile.isEmpty() || time < profile.firstKey() || time > profile.lastKey())
            return 0;
        return profile.floorEntry(time).getValue();
    }

    /**
     * Gets the available Ressources of the specified time.
     *
     * @param time the time of which to get the available ressources.
     * @return the available ressources or max. Cost if no cost where set.
     */
    public long availableRessources(long time)
    {
        return maxCapacity - cost(time);
    }

    /**
     * Gets the next time at which the cost changes or the current time if there
     * is no such point of time.
     *
     * @param start time of the present Job
     * @return the smalest time in which the total cost differs from the cost at
     * the given start time, or the start time if no such jump exists.
     */
    public long next(long start)
    {
        long cost = cost(start);
        Long next = _next(start, cost);
        return next == null ? start : next;
    }

    private Long _next(Long start, long cost)
    {
        start = profile.ceilingKey(start);

        if (start == null)
            return null;

        if (cost(start) != cost)
            return start;

        if (cost(++start) != cost)
            return start;

        return _next(start, cost);
    }

    /**
     * Gets the earliest time greater or equal to the currentTime at which the
     * cost can be increased for the while of the specified duration such that
     * the total cost is below the the capacity. Note that this method ensures
     *
     * @param start the time at which to start
     * @param duration the length of the intervall in which the value would be
     * increased
     * @param value the additional cost
     * @return the value of the next time in which such a value is pack
     */
    public long next(long start, long duration, long value)
    {
        if (value > maxCapacity)
            throw new IllegalArgumentException("The value is already higher then the capacity.");

        return _next(start, duration, maxCapacity - value);
    }

    private long _next(Long start, long duration, long delta)
    {
        if (start == null)
            return profile.lastKey() + 1;

        if (cost(start) > delta)
            return _next(profile.higherKey(start), duration, delta);

        Long end = start + duration;
        Long next = start;
        while ((next = profile.higherKey(next)) != null && next <= end)
            if (cost(next) > delta)
                return _next(profile.higherKey(next), duration, delta);
        return start;
    }

    /**
     * Removes all entries from this ressource profile.
     */
    public void clear()
    {
        profile.clear();
        minAvailableResources = 0;
    }

    @Override
    public String toString()
    {
        return resource + " [" + minAvailableResources + "|" + maxCapacity + "]";
    }

}